﻿/// <reference path="TsLinq.d.ts" />
/// <reference path="Dictionary.ts" />
var Linq = (function () {
    //
    // constructor
    //
    function Linq(items) {
        this._items = items || [];
    }
    //
    //  Returns one element which is fitting the expression.
    //  If no expression was defined, it takes the only argument in list. In this case there can be only one element in the list, otherwise an error is thrown.
    //  If nothing is found or not item is in the list, or the expression matches to more than one element, the method throws an error.
    //
    Linq.prototype.single = function (expr) {
        var item = this.singleOrDefault(expr, undefined);
        if (item === null) {
            throw new Error('No matching element or no elements in the list or maybe too much matching elements');
        }
        return item;
    };

    //
    //  Sums the collection. If an selector is specified, it is used to get the value to sum.
    //
    Linq.prototype.sum = function (selector) {
        var summed = 0;
        this.forEach(function (item, index) {
            summed += selector !== undefined ? selector(item, index) : item;
        });
        return summed;
    };

    //
    //  Returns one element which is fitting the expression.
    //  If no expression was defined, it takes the only argument in list. In this case there can be only one element in the list, otherwise an error is thrown.
    //  If nothing is matched, the method returns null. If there is more than one element matching, the method throws an error.
    //
    Linq.prototype.singleOrDefault = function (expr, defaultValue) {
        defaultValue = defaultValue || null;
        var l = this._items.length;
        if (l == 0) {
            return defaultValue;
        }
        if (expr == undefined) {
            if (this._items.length > 1) {
                throw new Error('single() without expression failed because there are more than one element in the collection.');
            }
            return this._items[0];
        }
        var foundItem = null;
        for (var i = 0; i < l; i++) {
            var item = this._items[i];
            if (expr(item, i)) {
                if (foundItem != null) {
                    throw new Error('single() failed because the expression matches more than one element');
                }
                foundItem = item;
            }
        }
        if (foundItem == null) {
            return defaultValue;
        }
        return foundItem;
    };

    //
    //  Returns the first element which is fitting the expression. If no expression was defined, it takes the first argument. If nothing is found or not item is in the list, the method throws an error.
    //
    Linq.prototype.first = function (expr) {
        var l = this._items.length;
        if (l == 0) {
            throw new Error('Collection contains no element');
        }
        if (expr == undefined)
            return this._items[0];
        for (var i = 0; i < l; i++) {
            var item = this._items[i];
            if (expr(item, i))
                return item;
        }
        throw new Error('first() does not fit any element which fits your expression');
    };

    //
    //  Returns the first item, or when a expression is given it returns the first item which is matching. If no items are existing
    //  or no item is matching, the method returns null.
    //
    Linq.prototype.firstOrDefault = function (expr) {
        if (this._items.length == 0) {
            return null;
        }
        if (expr === undefined) {
            return this._items[0];
        }
        var foundItem = null;
        this.forEach(function (item, index) {
            if (expr(item, index)) {
                foundItem = item;
                return false;
            }
        });
        return foundItem;
    };

    //
    //  Returns the last element which is fitting the expression. If no expression was defined, it takes the last item in the current list.
    //  If nothing is found or not item is in the list, the method throws an error.
    //
    Linq.prototype.last = function (expr) {
        var l = this._items.length;
        if (expr !== undefined) {
            var foundEntry = null;
            for (var i = l - 1; i >= 0; i--) {
                var item = this._items[i];
                if (expr(item, i) === true) {
                    return item;
                }
            }
            throw new Error('last() does not fit any element which fits your expression');
        }
        if (l == 0) {
            throw new Error('Collection contains no element.');
        }
        return this._items[l - 1];
    };

    //
    //  Returns the last element which is fitting the expression. If no expression was defined, it takes the last item in the current list.
    //  If nothing is found or not item is in the list, the method returns null.
    //
    Linq.prototype.lastOrDefault = function (expr, defaultValue) {
        var l = this._items.length;
        if (expr !== undefined) {
            for (var i = l - 1; i >= 0; i--) {
                var item = this._items[i];
                if (expr(item, i) === true) {
                    return item;
                }
            }
            return defaultValue || null;
        }
        if (l == 0) {
            return defaultValue || null;
        }
        return this._items[l - 1];
    };

    //
    //  Returns a list of values which are produced by the mapper function for every containing item.
    //
    Linq.prototype.select = function (mapper) {
        var resultSet = [];
        this.forEach(function (item, index) {
            return resultSet.push(mapper(item, index));
        });
        return new Linq(resultSet);
    };

    //
    //  Returns an value which indicates wherever all elements are matching the given expression.
    //
    Linq.prototype.all = function (expression) {
        return this.count() == this.count(expression);
    };

    //
    //  Returns a flatten list of values which are produced by the mapper function for every containing item.
    //
    Linq.prototype.selectMany = function (mapper) {
        var resultSet = [];
        this.forEach(function (item, index) {
            return resultSet.push(mapper(item, index));
        });
        var back = [];
        new Linq(resultSet).forEach(function (item) {
            var i = item;
            if (i.length != undefined) {
                for (var c = 0; c < i.length; c++) {
                    var currentItem = i[c];
                    back.push(i[c]);
                }
            } else if (i instanceof Linq) {
                i.forEach(function (currentItem) {
                    return back.push(currentItem);
                });
            } else {
                throw new Error('selectMany() fails because of unknown subselect type');
            }
        });
        return new Linq(back);
    };

    //
    //  Returns all elements which are fitting the specified condition.
    //
    Linq.prototype.where = function (expr) {
        if (expr === undefined) {
            throw new Error('where() failed, no expression was given');
        }
        var resultSet = [];
        this.forEach(function (item, index) {
            if (expr(item, index) == true)
                resultSet.push(item);
        });
        return new List(resultSet);
    };

    //
    //  Returns the index of the given item. If an comparer was specified, it is used for finding the item.
    //  The method returns the 0 based index of the item. If it does not exists, the method returns -1.
    //
    Linq.prototype.indexOf = function (item, comparer) {
        var l = this._items.length;
        if (l == 0) {
            return -1;
        }
        var comp = comparer || new DefaultComparer();
        for (var i = 0; i < l; i++) {
            if (comp.compare(item, this._items[i]) == 0) {
                return i;
            }
        }
        return -1;
    };

    //
    //  Returns the element at the given position.
    //
    Linq.prototype.elementAt = function (position) {
        if (position >= this.count()) {
            throw new Error('Unaccessible position');
        }
        return this._items[position];
    };

    //
    //  Returns the element at the given position. If the position is not valid, it returns null.
    //
    Linq.prototype.elementAtOrDefault = function (position, defaultValue) {
        if (position >= this.count()) {
            return defaultValue || null;
        }
        return this._items[position];
    };

    //
    //  Iterates over all items and runs the given function. If the function returns false, the enumeration is stopped.
    //  The method returns true if the enumeration was not stopped, if the enumeration was stopped, the method returns false.
    //
    Linq.prototype.forEach = function (toDo) {
        if (toDo === undefined) {
            throw new Error('forEach() failed because of empty method');
        }
        var l = this._items.length;
        for (var i = 0; i < l; i++) {
            var item = this._items[i];
            var returnValue = toDo(item, i);
            if (returnValue === false) {
                return false;
            }
        }
        return true;
    };

    //
    //  Returns an value which indicates wherever the given value does exists in the collection. When a comparer is specified,
    //  it is used for comparing the elements while looping.
    //
    Linq.prototype.contains = function (value, comparer) {
        var l = this._items.length;
        if (l == 0) {
            return false;
        }
        var comp = comparer || new DefaultComparer();
        for (var i = 0; i < l; i++) {
            if (comp.compare(value, this._items[i]) == 0) {
                return true;
            }
        }
        return false;
    };

    //
    //  Returns a value wherever the enumeration contains a item which matches the given expression. If no expression is given,
    //  the method returns true when there is any item the collection.
    //
    Linq.prototype.any = function (expr) {
        if (expr === undefined) {
            return this._items.length > 0;
        }
        return this.firstOrDefault(expr) != null;
    };

    //
    //  Returns the current items as an array.
    //
    Linq.prototype.toArray = function () {
        return this._items;
    };

    //
    //  Counts all elements. If an expression is given, the system counts all elements which are fitting this expression.
    //
    Linq.prototype.count = function (expr) {
        if (expr === undefined) {
            return this._items.length;
        }
        var l = this._items.length, count = 0;
        for (var i = 0; i < l; i++) {
            if (expr(this._items[i], i)) {
                count++;
            }
        }
        return count;
    };

    //
    //  Casts all elements in the list and returns a new one. If an mapper function is specified, this is used for generating the output.
    //
    Linq.prototype.cast = function (mapMethod) {
        if (mapMethod === undefined) {
            return this;
        }
        return this.select(function (x, index) {
            return mapMethod(x, index);
        });
    };

    //
    //  Aggregates the current values with the given aggregate method.
    //  If seed is undefined, the first entry of the current list is the first seeding value and will not enumerated, otherwise the aggregate method
    //  starts with the first entry in the current list by using the value of seed as the first aggregate value.
    //
    Linq.prototype.aggregate = function (func, seed) {
        if (typeof seed === "undefined") { seed = undefined; }
        if (!this.any())
            return null;
        var aggregate = seed || this.first();
        var startSequence = seed === undefined ? this.skip(1) : this;
        startSequence.forEach(function (item) {
            aggregate = func(aggregate, item);
        });
        return aggregate;
    };

    //
    //  Returns an error without the elements on the index until count is reached.
    //
    Linq.prototype.skip = function (count) {
        var l = this._items.length;
        if (count >= l) {
            return new Linq();
        }
        var resultSet = [];
        for (var i = count; i < l; i++) {
            resultSet.push(this._items[i]);
        }
        return new Linq(resultSet);
    };

    //
    //  Groups the elements into a dictionary. The indexer method creates the key for the dictionary.
    //
    Linq.prototype.toDictionary = function (indexer, keyComparer) {
        if (indexer === undefined) {
            throw new Error('toDictionary with empty indexer was called');
        }
        var resultSet = new Dictionary();
        this.forEach(function (item, index) {
            var itemIndex = indexer(item, index);
            var comp = keyComparer || new DefaultComparer();
            if (!resultSet.containsKey(itemIndex, comp)) {
                resultSet.add(itemIndex, [item]);
            } else {
                resultSet.get(itemIndex, keyComparer).push(item);
            }
        });
        return resultSet;
    };

    //
    //  Produces the set intersection of two sequences
    //
    Linq.prototype.intersect = function (intersectList, comparer) {
        if (intersectList instanceof Linq || intersectList instanceof List) {
            intersectList = intersectList.toArray();
        }
        var isectList = new Linq(intersectList);
        var comp = comparer || new DefaultComparer();
        return this.where(function (i) {
            return isectList.contains(i, comp);
        }).distinct();
    };

    //
    //  Reverts all items and returns them in reverted order.
    //
    Linq.prototype.reverse = function () {
        var resultSet = new List();
        var l = this._items.length - 1;
        for (var i = l; i >= 0; i--) {
            resultSet.push(this._items[i]);
        }
        return resultSet;
    };

    //
    //  Returns the average value of the items. If a selector was specified, this is used for getting values to cumulate.
    //
    Linq.prototype.average = function (selector) {
        var count = 0, summed = 0, index = 0;
        this.forEach(function (item) {
            var val = selector === undefined ? parseInt(item.toString()) : selector(item, index);
            if (val != null && val.toString() != 'NaN') {
                count++;
                summed += val;
            }
            index++;
        });
        if (count == 0)
            return 0;
        return summed / count;
    };

    //
    //  Returns all elements of the current list which are not contained in the given one.
    //
    Linq.prototype.except = function (exceptionList, comparer) {
        if (exceptionList instanceof Linq || exceptionList instanceof List) {
            exceptionList = exceptionList.toArray();
        }
        var isectList = new Linq(exceptionList);
        var comp = comparer || new DefaultComparer();
        return this.where(function (i) {
            return !isectList.contains(i, comp);
        }).distinct();
    };

    //
    //  Joins the two lists by adding the elements. When a zipper was specified, this method is used to join two elements.
    //
    Linq.prototype.zip = function (zipWith, zipper) {
        var tilIndex = new Linq([this._items.length, zipWith.length]).max() - 1;
        var resultSet = [];
        this.forEach(function (item, index) {
            var value = zipper === undefined ? item + zipWith[index] : zipper(item, zipWith[index]);
            resultSet.push(value);
            if (index > tilIndex)
                return false;
        });
        return new Linq(resultSet);
    };

    //
    //  Returns the minimal value of all items. You can specify a comparer for doing the comparision.
    //
    Linq.prototype.min = function (selector, comparer) {
        var comp = comparer || new DefaultComparer();
        var min = null;
        this.forEach(function (item, index) {
            var val = selector !== undefined ? selector(item, index) : item;
            if (min == null)
                min = val;
            else {
                var result = comp.compare(val, min);
                if (result < 0) {
                    min = val;
                }
            }
        });
        return min;
    };

    //
    //  Returns the maximal value of all items. You can specify a comparer for doing the comparision.
    //
    Linq.prototype.max = function (selector, comparer) {
        var comp = comparer || new DefaultComparer();
        var max = null;
        this.forEach(function (item, index) {
            var val = selector !== undefined ? selector(item, index) : item;
            if (max == null)
                max = val;
            else {
                var result = comp.compare(val, max);
                if (result > 0) {
                    max = val;
                }
            }
        });
        return max;
    };

    //
    //  Conats the two lists.
    //
    Linq.prototype.concat = function (withSource) {
        if (withSource instanceof Linq || withSource instanceof List) {
            var items = this._items.concat(withSource.toArray());
            return new Linq(items);
        } else if (withSource instanceof Array) {
            var items = this._items.concat(withSource);
            return new Linq(items);
        } else
            throw new Error('Unknown object type used for concat.');
    };

    //
    //  Returns a distinct list of all elements.
    //
    Linq.prototype.distinct = function (comparer) {
        var comp = comparer || new DefaultComparer();
        var resultSet = [];
        this.forEach(function (item) {
            if (resultSet.length == 0) {
                resultSet.push(item);
            } else {
                var found = false;
                for (var i = 0; i < resultSet.length; i++) {
                    var resItem = resultSet[i];
                    if (comp.compare(item, resItem) == 0) {
                        found = true;
                    }
                    if (found)
                        break;
                }
                if (!found) {
                    resultSet.push(item);
                }
            }
        });
        return new Linq(resultSet);
    };

    //
    //  Returns the first count elements of the list.
    //
    Linq.prototype.take = function (count) {
        var l = this._items.length;
        var takeIndex = count < l ? count : l;
        var resultSet = [];
        for (var i = 0; i < takeIndex; i++) {
            resultSet.push(this._items[i]);
        }
        return new List(resultSet);
    };

    //
    //  Returns a distinct list of the join of both lists, so every element is containing only one times.
    //
    Linq.prototype.union = function (listToAdd, comparer) {
        return this.concat(listToAdd).distinct(comparer);
    };

    //
    //  Returns a grouped list of the given items. The groupMapper parameter contains a method where the grouped output
    //  is generated. The optional key mapper and key comparer helps generating an comparing the key which are grouped.
    //
    Linq.prototype.groupBy = function (keyMapper, elementSelector, resultSelector, keyComparer) {
        var dict = this.toDictionary(keyMapper || function (item) {
            return item;
        }, keyComparer);
        elementSelector = elementSelector || function (item, index) {
            return item;
        };
        var resultDict = new Dictionary();
        dict.getKeys().forEach(function (key) {
            var values = dict.get(key);
            resultDict.add(key, resultSelector != undefined ? resultSelector(values, key) : values);
        });
        return resultDict;
    };

    //
    //  Orders the items by the given expression. You can specify a comparer for comparing the resulting expression values.
    //
    Linq.prototype.orderBy = function (orderByKeyMethod, comparer) {
        var dict = this.toDictionary(orderByKeyMethod, comparer);
        var comp = comparer || new JsonStringComparer();
        var keysSorted = dict.getKeys().sort(function (k1, k2) {
            return comp.compare(k1, k2);
        });
        var resultSet = [];
        keysSorted.forEach(function (key) {
            var values = new Linq(dict.get(key));
            values.forEach(function (i) {
                return resultSet.push(i);
            });
        });
        return new Linq(resultSet);
    };

    //
    //  Orders the items descending by the given expression. You can specify a comparer for comparing the resulting expression values.
    //
    Linq.prototype.orderByDescending = function (orderByKeyMethod, comparer) {
        var dict = this.toDictionary(orderByKeyMethod, comparer);
        var comp = comparer || new JsonStringComparer();
        var keysSorted = dict.getKeys().sort(function (k1, k2) {
            var res = comp.compare(k1, k2);
            if (res == 1)
                return -1;
            if (res == 0)
                return 0;
            if (res == -1)
                return 1;
            throw new Error('Comparer result of <' + res + '> is not valid, only 1, 0 and -1 are allowed.');
        });
        var resultSet = [];
        keysSorted.forEach(function (key) {
            var values = new Linq(dict.get(key));
            values.forEach(function (i) {
                return resultSet.push(i);
            });
        });
        return new Linq(resultSet);
    };

    //
    //  Returns the given default IEnumerable when the current collection is empty.
    //
    Linq.prototype.defaultIfEmpty = function (defaultValue) {
        if (this.count() == 0)
            return defaultValue;
        return this;
    };
    return Linq;
})();
